// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <tuple>

#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "content/common/frame_messages.h"
#include "content/public/test/render_view_test.h"
#include "content/renderer/render_frame_impl.h"
#include "content/renderer/render_view_impl.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/WebKit/public/platform/WebSize.h"
#include "third_party/WebKit/public/web/WebView.h"

// Tests for the external select popup menu (Mac specific).

namespace content {
namespace {

    const char* const kSelectID = "mySelect";
    const char* const kEmptySelectID = "myEmptySelect";

} // namespace

class ExternalPopupMenuTest : public RenderViewTest {
public:
    ExternalPopupMenuTest() { }

    RenderViewImpl* view()
    {
        return static_cast<RenderViewImpl*>(view_);
    }

    RenderFrameImpl* frame()
    {
        return view()->GetMainRenderFrame();
    }

    void SetUp() override
    {
        RenderViewTest::SetUp();
        // We need to set this explictly as RenderMain is not run.
        blink::WebView::setUseExternalPopupMenus(true);

        std::string html = "<select id='mySelect' onchange='selectChanged(this)'>"
                           "  <option>zero</option>"
                           "  <option selected='1'>one</option>"
                           "  <option>two</option>"
                           "</select>"
                           "<select id='myEmptySelect'>"
                           "</select>";
        if (ShouldRemoveSelectOnChange()) {
            html += "<script>"
                    "  function selectChanged(select) {"
                    "    select.parentNode.removeChild(select);"
                    "  }"
                    "</script>";
        }

        // Load the test page.
        LoadHTML(html.c_str());

        // Set a minimum size and give focus so simulated events work.
        view()->GetWidget()->GetWebWidget()->resize(blink::WebSize(500, 500));
        view()->GetWidget()->GetWebWidget()->setFocus(true);
    }

    int GetSelectedIndex()
    {
        base::string16 script(base::ASCIIToUTF16(kSelectID));
        script.append(base::ASCIIToUTF16(".selectedIndex"));
        int selected_index = -1;
        ExecuteJavaScriptAndReturnIntValue(script, &selected_index);
        return selected_index;
    }

protected:
    virtual bool ShouldRemoveSelectOnChange() const { return false; }

    DISALLOW_COPY_AND_ASSIGN(ExternalPopupMenuTest);
};

// Normal case: test showing a select popup, canceling/selecting an item.
TEST_F(ExternalPopupMenuTest, NormalCase)
{
    IPC::TestSink& sink = render_thread_->sink();

    // Click the text field once.
    EXPECT_TRUE(SimulateElementClick(kSelectID));

    // We should have sent a message to the browser to show the popup menu.
    const IPC::Message* message = sink.GetUniqueMessageMatching(FrameHostMsg_ShowPopup::ID);
    ASSERT_TRUE(message != NULL);
    std::tuple<FrameHostMsg_ShowPopup_Params> param;
    FrameHostMsg_ShowPopup::Read(message, &param);
    ASSERT_EQ(3U, std::get<0>(param).popup_items.size());
    EXPECT_EQ(1, std::get<0>(param).selected_item);

    // Simulate the user canceling the popup; the index should not have changed.
    frame()->OnSelectPopupMenuItem(-1);
    EXPECT_EQ(1, GetSelectedIndex());

    // Show the pop-up again and this time make a selection.
    EXPECT_TRUE(SimulateElementClick(kSelectID));
    frame()->OnSelectPopupMenuItem(0);
    EXPECT_EQ(0, GetSelectedIndex());

    // Show the pop-up again and make another selection.
    sink.ClearMessages();
    EXPECT_TRUE(SimulateElementClick(kSelectID));
    message = sink.GetUniqueMessageMatching(FrameHostMsg_ShowPopup::ID);
    ASSERT_TRUE(message != NULL);
    FrameHostMsg_ShowPopup::Read(message, &param);
    ASSERT_EQ(3U, std::get<0>(param).popup_items.size());
    EXPECT_EQ(0, std::get<0>(param).selected_item);
}

// Page shows popup, then navigates away while popup showing, then select.
TEST_F(ExternalPopupMenuTest, ShowPopupThenNavigate)
{
    // Click the text field once.
    EXPECT_TRUE(SimulateElementClick(kSelectID));

    // Now we navigate to another pager.
    LoadHTML("<blink>Awesome page!</blink>");

    // Now the user selects something, we should not crash.
    frame()->OnSelectPopupMenuItem(-1);
}

// An empty select should not cause a crash when clicked.
// http://crbug.com/63774
TEST_F(ExternalPopupMenuTest, EmptySelect)
{
    EXPECT_TRUE(SimulateElementClick(kEmptySelectID));
}

class ExternalPopupMenuRemoveTest : public ExternalPopupMenuTest {
public:
    ExternalPopupMenuRemoveTest() { }

protected:
    bool ShouldRemoveSelectOnChange() const override { return true; }
};

// Tests that nothing bad happen when the page removes the select when it
// changes. (http://crbug.com/61997)
TEST_F(ExternalPopupMenuRemoveTest, RemoveOnChange)
{
    // Click the text field once to show the popup.
    EXPECT_TRUE(SimulateElementClick(kSelectID));

    // Select something, it causes the select to be removed from the page.
    frame()->OnSelectPopupMenuItem(0);

    // Just to check the soundness of the test, call SimulateElementClick again.
    // It should return false as the select has been removed.
    EXPECT_FALSE(SimulateElementClick(kSelectID));
}

class ExternalPopupMenuDisplayNoneTest : public ExternalPopupMenuTest {
public:
    ExternalPopupMenuDisplayNoneTest() { }

    void SetUp() override
    {
        RenderViewTest::SetUp();
        // We need to set this explictly as RenderMain is not run.
        blink::WebView::setUseExternalPopupMenus(true);

        std::string html = "<select id='mySelect'>"
                           "  <option value='zero'>zero</option>"
                           "  <optgroup label='hide' style='display: none'>"
                           "    <option value='one'>one</option>"
                           "  </optgroup>"
                           "  <option value='two'>two</option>"
                           "  <option value='three'>three</option>"
                           "  <option value='four'>four</option>"
                           "  <option value='five'>five</option>"
                           "</select>";
        // Load the test page.
        LoadHTML(html.c_str());

        // Set a minimum size and give focus so simulated events work.
        view()->GetWidget()->GetWebWidget()->resize(blink::WebSize(500, 500));
        view()->GetWidget()->GetWebWidget()->setFocus(true);
    }
};

TEST_F(ExternalPopupMenuDisplayNoneTest, SelectItem)
{
    IPC::TestSink& sink = render_thread_->sink();

    // Click the text field once to show the popup.
    EXPECT_TRUE(SimulateElementClick(kSelectID));

    // Read the message sent to browser to show the popup menu.
    const IPC::Message* message = sink.GetUniqueMessageMatching(FrameHostMsg_ShowPopup::ID);
    ASSERT_TRUE(message != NULL);
    std::tuple<FrameHostMsg_ShowPopup_Params> param;
    FrameHostMsg_ShowPopup::Read(message, &param);
    // Number of items should match item count minus the number
    // of "display: none" items.
    ASSERT_EQ(5U, std::get<0>(param).popup_items.size());

    // Select index 1 item. This should select item with index 2,
    // skipping the item with 'display: none'
    frame()->OnSelectPopupMenuItem(1);

    EXPECT_EQ(2, GetSelectedIndex());
}

} // namespace content
